



import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.channels.FileChannel;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.*;



public class ControlServer implements Runnable {

	//stores the filename to chunks mapping. Eg: filename abc.txt will be stored as key
	// and the chunks abc_1, abc_2, etc. will be stored in the Vector<> as values.
	public static Hashtable<String, Vector<String>> fileToChunkMap = new Hashtable<String, Vector<String>>();

	//stores the data server info.
	public static Hashtable<String, Vector<DataServerInfo>> dataServerList = new Hashtable<String, Vector<DataServerInfo>>();

	public static Hashtable<String, Vector<String>> chunkToDataServerMap = new Hashtable<String, Vector<String>>();


	private ServerSocketChannel server;
	private SocketChannel socket;
	private final String CONFIRMED ="CONFIRMED";
	private final String DENIED ="DENIED";
	private static int counter = 1;

	
	/**
	 * Constructor
	 * @param port
	 * @throws IOException
	 */
	public ControlServer(int port) throws IOException{
		this.server = ServerSocketChannel.open();
		this.server.socket().bind(new InetSocketAddress(port));
		//this.fileToChunkMap = new Hashtable<String, Vector<String>>();
	}

	/**
	 * Looks up the file name for the respective chunks and sends it 
	 * back to the calling host. The method will send the chunk name 
	 * as well as the server address of the data server where the 
	 * chunk resides.
	 * 
	 * @param fileName - The name of the file.
	 * @return
	 */
	public Hashtable<String,Vector<String>> lookUpChunks(String fileName){
		Hashtable<String, Vector<String>> chunkList = new Hashtable<String, Vector<String>>();
		Vector<String> dataServerName;
		if(fileToChunkMap.get(fileName) != null){

			dataServerName = new Vector<String>();
			dataServerName = fileToChunkMap.get(fileName);

			for(Enumeration e = dataServerName.elements(); e.hasMoreElements();){	
				String s = (String)e.nextElement();
				if(chunkToDataServerMap.get(s) != null){
					chunkList.put(s, chunkToDataServerMap.get(s));
				}else{
					System.err.println("Chunk Missing: <info> -FileName: " + fileName + " -chunkID: " + s);
					System.err.println(" System exiting...");
					System.exit(0);
				}	
			}
			return chunkList;
		}else{
			System.err.println("No such file found..");
			return null;
		}
	}

	/**
	 * 
	 * This method is used to ping all the respective data servers who have the requested file's
	 * chunks stored. It tells the data server to be prepared for the chunk request.
	 * 
	 * @param chunkInfoList
	 */
	public void pingDataServers(){
		Vector<DataServerInfo> dataServer = new Vector<DataServerInfo>();

		for(Enumeration e = dataServerList.keys(); e.hasMoreElements();){

			String key = (String) e.nextElement();
			dataServer = dataServerList.get(key);
			DataServerInfo dsHostPort = dataServer.elementAt(0);
			new DataServerCommunication(key, dsHostPort).start();
		}	
	}

	/**
	 * 
	 * This method will add the data server into its list of available servers.
	 * The data server will initiate a connection to the control server and then pass the 
	 * ServerSocketChannel object which will be eventually passed to the client so that a 
	 * file I/O connection can be initiated.
	 * 
	 * @param dsInfo
	 * @param server
	 * @throws IOException
	 */
	public void addDataServer(DataServerInfo dsInfo) throws IOException{
		Vector<DataServerInfo> dataVector = new Vector<DataServerInfo>();
		String dsName = "ds_" + counter;
		System.out.println(dsName + " *********");
		if(dataServerList.get(dsName) == null){
			dsInfo.setDataServerName(dsName);
			dataVector.add(0, dsInfo);
			dataServerList.put(dsName,dataVector );
			counter = counter + 1;
			//dsInfo.getObjOutputStream().writeUnshared(CONFIRMED);
			System.out.println("Confirmed");
		}else{
			System.out.println("Denied");
			//dsInfo.getObjOutputStream().writeUnshared(DENIED);
		}

		System.out.println(dataServerList);
	}
	
	/**
	 * This method initiates the transfer from the FileServer and transfers control 
	 * to the FileSplitService class which will further split the files and send it
	 * to respective data servers. 
	 * @param socket - The incoming client socket of the SocketChannel class on which 
	 * the transfer is made.
	 * @param fileName - The name of the file being transferred.
	 * @param size - The size of the transferred file.
	 * @throws IOException
	 */
	public void startFileServerCommunication(SocketChannel socket, 
			String fileName, long size) throws IOException {
		FileOutputStream outFile = new FileOutputStream(fileName);
		FileChannel incomingFile = outFile.getChannel();
		System.out.println("Initiating file transfer "+ size);

		incomingFile.transferFrom(socket, 0, size);
		System.out.println("File successfully transferred");
		new FileSplitService(fileName).start();
	}
	
	@Override
	/**
	 * run method
	 * 
	 */
	public void run() {
		try{
			DataServerInfo dsInfo = null;
			System.out.println("Waiting for client... ");
			
			while(true){
				socket = server.accept();
				ObjectInputStream in = new ObjectInputStream(socket.socket().getInputStream());
				ObjectOutputStream out = new ObjectOutputStream(socket.socket().getOutputStream());	

				Object incoming = in.readUnshared();
				System.out.println(incoming.toString());
				if(incoming == null){
					System.out.println("NULL******");
				}

				if(incoming instanceof String){
					if(incoming.equals("FileServer")){

						String fileName = (String)in.readUnshared();

						Long size = (Long)in.readUnshared();

						System.out.println("Receiving file "+fileName+" from file server...");
						startFileServerCommunication(socket, fileName, size);
					}else if(incoming.equals("Data Server")){
						System.out.println("Data server request");
						DataServerInfo ds = new DataServerInfo(socket);
						ds.setServerHost((String)in.readUnshared());
						ds.setServerPort((Integer)in.readUnshared());
						ds.setObjInputStream(in);
						ds.setObjOutputStream(out);
						System.out.println("Adding data server to database");
						addDataServer(ds);
					}else if(incoming.equals("Client")){
						Hashtable<String, Vector<String>> chunkList = new Hashtable<String, Vector<String>>();
						String fileRequestedByClient = (String)in.readUnshared();
						System.out.println("Client requested file: " + fileRequestedByClient);
						chunkList = lookUpChunks(fileRequestedByClient);
						out.writeUnshared(chunkList);
					}
					else{
						System.out.println("CONTROL SERVER: Incorrect String");
					}
				}else{
					System.out.println("Invalid");
					System.exit(0);
				}
			}
		}catch(IOException e){
			if(socket == null){
				System.err.println("CONTROL SERVER: Client Socket disconnected. Preparing for shutdown");
				try{
					socket.close();
				}catch(Exception eA){
					eA.printStackTrace();
				}
			}
			e.printStackTrace();
		}catch(ClassNotFoundException eX){
			System.err.println("CONTROL SERVER: Error on incoming object. Exiting..");
		}
		finally{
			try{
				socket.close();
			}catch(Exception e){
				e.printStackTrace();
			}
		}
	}

	/**
	 * Main method
	 * @param args
	 */
	public static void main(String args[]){
		if(args.length != 1){
			System.err.println("Usage: java ControlServer <port>");
			System.exit(0);
		}
		int port = Integer.parseInt(args[0]);

		try {
			ControlServer controlServer = new ControlServer(port);
			Thread t = new Thread(controlServer); 
			t.start();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}